SPB Git

Repository Public

route.ts 2.0 KB 48 lines typescript

1// Auteur : Simon-Pierre Boucher — contact@spboucher.ai2// /api/admin/attachments/[id] — téléchargement d'une pièce jointe.3// Les fichiers vivent HORS de /public (data/contact-uploads) : seul un admin4// autorisé à voir la demande peut les lire. Nom stocké aléatoire — le nom5// d'origine n'est restitué que dans l'en-tête Content-Disposition.6import { NextRequest, NextResponse } from "next/server";7import fs from "fs/promises";8import path from "path";9import { findAttachment, findSubmissionById } from "@/lib/comms/db";10import { getAdminSession, canViewSubmission } from "@/lib/comms/admin-auth";1112const UPLOAD_DIR = path.join(process.cwd(), "data", "contact-uploads");1314export async function GET(15  _req: NextRequest,16  { params }: { params: Promise<{ id: string }> },17) {18  const session = await getAdminSession();19  if (!session)20    return NextResponse.json({ error: "Non autorisé." }, { status: 401 });21  const id = Number((await params).id);22  const att = Number.isInteger(id) ? findAttachment(id) : undefined;23  const sub = att ? findSubmissionById(att.submission_id) : undefined;24  if (!att || !sub || !canViewSubmission(session, sub))25    return NextResponse.json({ error: "Fichier introuvable." }, { status: 404 });2627  // stored_name est produit par nous ("2026/KA-…-abcd.pdf") — on neutralise28  // quand même toute traversée de chemin.29  const safe = path.normalize(att.stored_name).replace(/^(\.\.[/\\])+/, "");30  const full = path.join(UPLOAD_DIR, safe);31  if (!full.startsWith(UPLOAD_DIR))32    return NextResponse.json({ error: "Chemin refusé." }, { status: 400 });33  let buf: Buffer;34  try {35    buf = await fs.readFile(full);36  } catch {37    return NextResponse.json({ error: "Fichier manquant sur le disque." }, { status: 404 });38  }39  const filename = att.original_name.replace(/["\r\n]/g, "");40  return new NextResponse(new Uint8Array(buf), {41    headers: {42      "Content-Type": att.mime,43      "Content-Disposition": `attachment; filename="${filename}"`,44      "Cache-Control": "private, no-store",45    },46  });47}48
Files main
Menu

Theme